#
# $Id$
#
# RTEMS Project (http://www.rtems.org/)
# Copyright 2007 Chris Johns (chrisj@rtems.org)
# All rights reserved.
#
# This file is part of The Builder.
#
# The Builder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The Builder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with The Builder; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# Builder Configuration files handler.
#

import os
import re
import string

import configparse
import error

class data:
    def __init__(self):
        # Pattern to match a quoted string
        self.qstr = re.compile('[rR]?\'([^\\n\'\\\\]|\\\\.)*\'|[rR]?"([^\\n"\\\\]|\\\\.)*"')
        self.input = None
        self.macros = {}
        self.tokens = None
        self.groups = {}

    def __str__(self):
        return 'Configuration:' + os.linesep + self.format(True)
    
    def find_loc(self, pos):
        """Find the location in the configuration files for
        a specific location in the input buffer. We need to reverse
        back in the input data until we find the %%bcpp-line-info%%
        tag which defines the local context of the pos."""
        start = self.input[:pos].rfind('%%bcpp-line-info%%')
        if start < 0:
            raise error.internal('invalid position or preprocessed config data')
        start -= 2
        section = self.input[start:pos]
        info = section.splitlines()[0].split()
        if info[0] != '#' or info[1] != '%%bcpp-line-info%%' :
            raise error.internal('no line number marker found in preprocessed config')
        end = self.input[pos:].find('\n')
        if end < 0:
            end = len(input)
        else:
            end += pos
        section += self.input[pos:end]
        lc = int(info[2]) - 1
        pos -= start
        current = 0
        for line in section.splitlines():
            if (current + len(line)) >= pos:
                break
            lc += 1
            current += len(line) + 1
        return (info[3], lc, pos - current + 1, line)
    
    def ouch(self, pos, what):
        where = self.find_loc(pos)
        raise error.config(where, what)

    def numbered_lines(self, lines = True):
        output = ''
        if not self.input:
            output = 'No input'
        else:
            c = 1
            for l in self.input.splitlines():
                if lines:
                    output += ("%3d: %s" % (c, l)) + os.linesep
                else:
                    output += l + os.linesep
                c += 1
        return output 

    def is_element(self, token):
        elements = ('id', 'list', 'sep', 'fslash', 'bslash', 'leq', 'req',
                    'pipe', 'redirin', 'mref', 'str', 'num', 'ver', 'text')
        return type(token) is tuple and \
               len(token) == 3 and \
               token[0] in elements

    def dump(self, show_type = False, show_pos = False, tokens = None, indent = 0):
        """Dump the tokens to text output which shows the logical
        layout of the tokens."""
        if not tokens:
            tokens = self.tokens
        if tokens is None:
            return 'No configuration data loaded'
        leading_edge = True
        trailing_edge = False
        output = ''
        for t in tokens:
            if type(tokens) is dict:
                if leading_edge:
                    for c in range(0, indent):
                        output += ' '
                output += t + ':' + os.linesep + \
                          self.dump(show_type, show_pos, tokens[t], indent + 1)
                continue
            if type(t) is dict or type(t) is list or \
               (type(t) is tuple and not self.is_element(t)):
                if not leading_edge and not trailing_edge:
                    output += os.linesep
                if leading_edge:
                    output += self.dump(show_type, show_pos, t, indent)
                else:
                    output += self.dump(show_type, show_pos, t, indent + 1)
                trailing_edge = True
            else:
                if leading_edge:
                    for c in range(0, indent):
                        output += ' '
                s = None
                p = None
                if self.is_element(t):
                    if t[0] == 'num' or t[0] == 'ver':
                        s = str(t[1])
                    else:
                        s = t[1]
                    if show_type:
                        s = t[0] + '(' + s + ')'
                    p = t[2]
                else:
                    if type(t) is int:
                        p = t
                    else:
                        s = t
                if leading_edge and self.is_element(t):
                        output += ' '
                if s:
                    output += s
                if show_pos and p:
                    where = self.find_loc(p)
                    output += '[%s:%i,%i]' % (where[0], where[1], where[2])
                if leading_edge:
                    if not self.is_element(t):
                        output += ':'
                    leading_edge = False
                output += ' '
                trailing_edge = False
        if not leading_edge and not trailing_edge:
            output += os.linesep
        return output

    def format(self, show_type = False, show_pos = False, indent = 1):
        """Format the processed configuration to a string which shows the logical
        layout."""
        return self.dump(show_type, show_pos, self.groups, indent)
    
    def concatinate(self, input):
        """Join lines that end with the '\' character
        with following lines"""
        cont_char = re.compile('\\\\$')
        output = None
        # For each line see if there is any line continuation
        # then read each macro assignmment
        for line in input.splitlines():
            if cont_char.match(line):
                line = line[:-1]
            if output:
                output += '\n' + line
            else:
                output = line
        return output

    def find_token(self, token, line):
        """Find a token that is not part of a quoted string."""
        pos = line.find(token)
        if pos >= 0:
            # Could be in a quoted string.
            quoted = self.qstr.search(line)
            if quoted:
                if pos < quoted.start():
                    pos = quoted.start()
                elif pos <= quoted.end():
                    equals = -1
        return pos;

    def remove_comment(self, line):
        pos = self.find_token('#', line)
        if pos >= 0:
            line = line[:pos]
        return line

    def read(self, filename):
        """Read the file and scan the data looking for include
        files. Concatinate any lines. Add line number markers
        into the data to allow tracking of the file and line for
        errors."""
        try:
            f = open(filename, "rb")
            input = self.concatinate(f.read())
            f.close()
        except IOError:
            raise error.general('reading configuration file: ' + filename)
        include = re.compile('include')
        output = '# %%bcpp-line-info%% 1 ' + filename
        lineno = 1
        for line in input.splitlines():
            cleaned = self.remove_comment(line)
            matched = include.match(cleaned.strip())
            if matched:
                include_file = cleaned.strip()[matched.end():].strip()
                output += '\n' + self.read(include_file) + \
                          '\n# %%bcpp-line-info%% ' + \
                          str(lineno + 1) + ' ' + filename
            else:
                output += '\n' + line
            lineno += 1
        return output + '\n'

    def transform(self, element):
        """See if an element has a macro reference. If it does
        transform the macro or print an error"""
        data = element[1]
        if element[0] != 'str' and type(data) is str:
            repeat = True
            while repeat:
                repeat = False
                s = data.split('${', 1)
                if len(s) == 2:
                    front = s[0]
                    s = s[1].split('}', 1)
                    if len(s) == 2:
                        macro = '${' + s[0] + '}' 
                        if not macro in self.macros:
                            self.ouch(element[2], "macro '" + macro + \
                                      "' not defined")
                        data = front + str(self.macros[macro][1]) + s[1]
                        repeat = True
        return (element[0], data, element[2])

    def postprocess(self):
        """Transform the parsed tokens separating the macros
        from the tokens then resolve the references to the macros
        in the parsed tokens. Merge common groups and elements into
        one single entity."""
        for t in self.tokens[1]:
            if t[0] == 'macro':
                if t[1][1] in self.macros:
                    self.ouch(t[1][2], t[1][1] + 'already defined')
                self.macros[t[1][1]] = (t[2][0], t[2][1])
        # Handle references to macros in the macros.
        repeat = False # do not do this any more
        while repeat:
            repeat = False
            for m in self.macros:
                macro = self.macros[m] 
                if macro[0] == 'mref' and macro[1] in self.macros:
                    if m == self.macros[m][1]:
                        raise self.ouch(self.macros[m][2],
                                        'self referencing macro: ' + m)
                    self.macros[m] = self.macros[self.macros[m][1]]
                    repeat = True
        # Create a dict of groups merging common named groups together.
        # The dict contains a dict of members of each group.
        for g in self.tokens[1]:
            if g[0] == 'group':
                group = self.transform(g[1])[1]
                if not group in self.groups:
                    self.groups[group] = {}
                for m in g[2]:
                    member = self.transform(m[1])[1]
                    if not member in self.groups[group]:
                        self.groups[group][member] = []
                    elements = []
                    for e in m[2]:
                        elements.append(self.transform(e))
                    self.groups[group][member].extend(elements)

    def load(self, filename, save_temps = False):
        """Load the configuration from a file."""
        self.input = self.read(filename)
        if save_temps:
            root, ext = os.path.splitext(filename)
            try:
                f = open(root + '.i', 'w+')
                f.write(self.numbered_lines(self.input))
                f.close()
            except IOError:
                raise error.general('could write processed output file: ' + \
                                    root + '.i')
        scanner = configparse.BuilderConfigurationScanner(self.input)
        parser = configparse.BuilderConfiguration(scanner)
        try:
            self.tokens = getattr(parser, 'Config')()
        except configparse.SyntaxError, se:
            raise error.config(self.find_loc(se.pos), se.msg)
        except configparse.NoMoreTokens:
            raise error.internal('could not complete parsing around here: ' + \
                                 str(parser._scanner))
        self.postprocess()

    def get_group_names(self):
        """Get the names of the groups"""
        return self.groups.keys()

    def get_group(self, name):
        """Get a group that matches the name"""
        group = {}
        if name in self.groups:
            for member in self.groups[name]:
                group[member] = [e for e in self.groups[name][member]]
        return group

    def get_member_names(self, group):
        """Get the names of the members of a group"""
        if group in self.groups:
            return self.groups[group].keys()
        return []

    def get_member(self, group, member):
        """Get the member of a group"""
        if group in self.groups:
            if member in self.groups[group]:
                return [m for m in self.groups[group][member] if m[0] != 'list']
        return []

    def has_group(self, group):
        """See if the group exists"""
        if group in self.groups.keys():
            return True
        return False

    def has_member(self, group, member):
        """See if the group has a member"""
        if member in self.get_member_names(group):
            return True
        return False

    def has_element(self, group, member, element, nature = None):
        """See if the element is present as a member of a group."""
        m = self.get_member(group, member)
        if m:
            for e in m:
                if element == e[1]:
                    if nature and not nature == e[0]:
                        continue
                    return True
        return False               

    def follow(self, group, member, match):
        """Follow a group::member element until is matches. The method allows
        a group::member to reference another group::member or group so you
        link to common configuration entry."""
        elements = []
        visited = []
        pos = -1
        while True:
            if group + '::' + member in visited:
                raise self.ouch(pos, 'configuration sequence loops at group::member: ' +
                                group + '::' + member)
            if not self.has_member(group, member):
                if pos >= 0:
                    self.ouch(pos, 'cannot find group::member: ' +
                              group + '::' + member)
                raise error.general('cannot find group::member: ' +
                                    group + '::' + member)
            elements = self.get_member(group, member)
            if len(elements) == 0 or match(elements[0][1]):
                break
            visited.append(group + '::' + member)
            s = elements[0][1].split('::')
            pos = elements[0][2]
            if len(s) == 1:
                group = s[0]
            elif len(s) == 2:
                if len(s[0]):
                    group = s[0]
                if len(s[1]):
                    member = s[1]
            else:
                self.ouch(pos, 'invalid group::member format')
        return elements

if __name__ == "__main__":
    def follow_match(this):
        return this == 'match-this'
    try:
        c = data()
        c.load('test/test1.cf')
        print 'Dump Test Config:'
        print c.dump(True)
        print 'Test Config:'
        print c.format(True)
        print 'Get o2 members: ' + str(c.get_member_names('o2'))
        print 'Get o2: ' + str(c.get_group('o2'))
        print 'Get o1:e1_5 ' + str(c.get_member('o1', 'e1_5'))
        print 'Has o1 got e1_4: ' + str(c.has_member('o1', 'e1_4'))
        print 'Has o1 got e9_1: ' + str(c.has_member('o1', 'e9_1'))
        print 'Has o1:e1_1 got m1_1_1_1: ' + \
              str(c.has_element('o1', 'e1_1', 'm1_1_1_1'))
        print 'Has o1:e1_1 got m1_1_1_1: ' + \
              str(c.has_element('o1', 'e1_1', 'm1_1_1_1'))
        print 'Has o1:e1_1 got m11: ' + \
              str(c.has_element('o1', 'e1_1', 'm11'))
        print 'Has o9:e1 got m1: ' + str(c.has_element('o9', 'e1', 'm1'))
        print 'Follow until match-this: ' + c.follow('o1', 'e1_4', follow_match)[0][1]
        del c
        c = data()
        # Generate a parsing error to check the line printing.
        print 'Parse a configuration with an error:'
        c.load('test/test2.cf')
    except error.error, e:
        print e
                       
